<template>
  <div>
    <h1>用户中心</h1>
    <div id="drag" ref="drag">
      <input type="file" name="file" @change="handlerFileChange">
    </div>
    <div>
      <div>
        <el-progress :stroke-width="20" :text-inside="true" :percentage="uploadProgress" />
      </div>
      <el-button @click="uploadFile">
        上传
      </el-button>
    </div>
    <div>
      <p>计算hash的进度条</p>
      <el-progress :stroke-width="20" :text-inside="true" :percentage="hashProgress" />
    </div>
    <!-- chunk.progress progress < 0 报错 显示红色 === 100 成功 别的数字 方块显示高亮 -->
    <!--尽可能让方块看起来为正方形 比如10个 4*4  100 10*10-->
    <div class="cube-container" :style="{width: cubeWidth + 'px'}">
      <div v-for="chunk in chunks" :key="chunk.index" class="cube">
        <div
          :class="{
            'uploading': chunk.progress > 0 && chunk.progress < 100,
            'success': chunk.progress === 100,
            'error': chunk.progress < 0,
          }"
          :style="{height: chunk.progress + '%'}"
        >
          <i v-if="chunk.progress > 1 && chunk.progress < 100" class="el-icon-loading" />
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import SparkMd5 from 'spark-md5'
const CHUNK_SIZE = 100 * 1024 // 0.5M
export default {
  data () {
    return {
      userInfo: {},
      hashProgress: 0,
      chunks: []
    }
  },
  computed: {
    cubeWidth () {
      return Math.ceil(Math.sqrt(this.chunks.length)) * 16
    },
    uploadProgress () {
      if (!this.file || this.chunks.length) {
        return 0
      }
      const loaded = this.chunks.map(item => item.chunks.size + item.progress).reduce((acc, cur) => acc + cur, 0)
      return Number((loaded * 100 / this.file.size).toFixed(2))
    }
  },
  mounted () {
    this.getUserInfo()
    this.bindEvent()
  },
  methods: {
    async getUserInfo () {
      const ret = await this.$http.get('/user/info')
      if (ret.code === 0) {
        this.userInfo = ret.data
      }
    },
    bindEvent () {
      const drag = this.$refs.drag
      drag.addEventListener('dragover', (e) => { // 拖拽 进入
        drag.style.borderColor = 'red'
        e.preventDefault()
      })
      drag.addEventListener('dragleave', (e) => { // 拖拽 移除
        drag.style.borderColor = '#eee'
        e.preventDefault()
      })
      drag.addEventListener('drop', (e) => { // 拖拽 释放后
      //  console.log(e.dataTransfer.files)
        const fileList = e.dataTransfer.files
        drag.style.borderColor = '#eee'
        this.file = fileList[0]
        e.preventDefault() // 阻止后显示文件名称 可以方式文件在浏览器打开
      })
    },
    handlerFileChange (e) {
      const [file] = e.target.files
      if (!file) { return }
      this.file = file
    },
    blobToString (blob) {
      return new Promise((resolve) => {
        const reader = new FileReader()
        reader.onload = function () {
          const ret = reader.result
            .split('')
            .map(v => v.charCodeAt())
            .map(v => v.toString(16).toUpperCase())
            .map(v => v.padStart(2, '0'))
            .join(' ')
          resolve(ret)
        }
        reader.readAsBinaryString(blob)
      })
    },
    async isGif (file) {
      // GIF89a GIF87a
      // GIF 6个16进制是 47 49 46 38 39 61 || 47 49 46 38 37 61
      const ret = await this.blobToString(file.slice(0, 6))
      // console.log(ret)
      const isGif = (ret === '47 49 46 38 39 61') || (ret === '47 49 46 38 37 61')
      return isGif
    },
    async isPng (file) {
      // PNG 8个16进制是 89 50 4E 47 0D 0A 1A 0A
      const ret = await this.blobToString(file.slice(0, 8))
      // console.log(ret)
      const isPng = ret === '89 50 4E 47 0D 0A 1A 0A'
      return isPng
    },
    async isJpg (file) {
      // JPG 2个16进制 和 后 2 个16进
      const len = file.size
      const start = await this.blobToString(file.slice(0, 2))
      const tail = await this.blobToString(file.slice(-2, len))
      const isJpg = start === 'FF D8' && tail === 'FF D9'
      return isJpg
    },
    async isImage (file) {
      // 通过文件流来判断
      return await this.isGif(file) || await this.isPng(file) || await this.isJpg(file)
    },
    // 大文件切片
    createFileChunk (file, size = CHUNK_SIZE) {
      const chunks = []
      let cur = 0
      while (cur < this.file.size) {
        chunks.push({
          index: cur,
          file: this.file.slice(cur, cur + size)
        })
        cur += size
      }
      return chunks
    },
    // 计算文件hash 方法一 webWorker
    calcuteHashWorker () {
      return new Promise((resolve) => {
        this.worker = new Worker('/hash.js')
        this.worker.postMessage({ chunks: this.chunks })
        this.worker.onmessage = (e) => {
          const { progress, hash } = e.data
          this.hashProgress = Number(progress.toFixed(2))
          if (hash) {
            resolve(hash)
          }
        }
      })
    },
    // 计算文件hash 方法二 Id   requestIdleCallback -react fiber源码应用
    calcuteHashIdle () {
      const chunks = this.chunks
      return new Promise((resolve) => {
        const spark = new SparkMd5.ArrayBuffer()
        let count = 0
        const apppendToSpark = (file) => {
          return new Promise((resolve) => {
            const reader = new FileReader()
            reader.readAsArrayBuffer(file)
            reader.onload = (e) => {
              spark.append(e.target.result)
              resolve()
            }
          })
        }
        const workLoop = async (deadline) => {
          while (count < chunks.length && deadline.timeRemaining() > 1) {
            // 空闲时间，且有任务
            await apppendToSpark(chunks[count].file)
            count++
            if (count < chunks.length) {
              this.hashProgress = Number(((100 * count) / chunks.length).toFixed(2))
            } else {
              this.hashProgress = 100
              resolve(spark.end())
            }
          }
          window.requestIdleCallback(workLoop)
        }
        window.requestIdleCallback(workLoop)
      })
    },
    // 抽样hash
    calcuteHashSample () {
      // 抽样hash
      // 布隆过滤器，损失一定的精度换取大量的性能提升
      return new Promise((resolve) => {
        const spark = new SparkMd5.ArrayBuffer()
        const reader = new FileReader()
        const file = this.file
        const size = file.size
        const offset = 2 * 1024 * 1024
        // 第一个2M ，最后一个区块数据全要
        // 中间的，去前后2个字节
        const chunks = [file.slice(0, offset)]
        let cur = offset
        while (cur < size) {
          if (cur + offset >= size) {
            chunks.push(file.slice(cur, cur + offset))
          } else {
            // 中间区块
            const mid = cur + offset / 2
            const end = cur + offset
            chunks.push(file.slice(cur, cur + 2))
            chunks.push(file.slice(mid, cur + mid + 2))
            chunks.push(file.slice(end - 2, end))
          }
          cur += offset
        }
        reader.readAsArrayBuffer(new Blob(chunks))
        reader.onload = (e) => {
          spark.append(e.target.result)
          this.hashProgress = 100
          resolve(spark.end())
        }
      })
    },
    async  uploadFile () {
      if (!this.file) {
        return
      }
      // console.log(this.file)
      // if (!await this.isImage(this.file)) { // 图片格式限制
      //   this.$message.error('文件格式不是图片')
      //   return
      // }
      // const this.chunks = this.createFileChunk()
      const chunks = this.createFileChunk(this.file)
      // const hash = await this.calcuteHashWorker()
      // const hash1 = await this.calcuteHashIdle()
      const hash = await this.calcuteHashSample()
      this.hash = hash
      // 问一下后端文件是否上传过，如果没有是否有上传的切片
      const { data: { uploaded, uploadedList } } = await this.$http.post('/checkfile', {
        hash: this.hash,
        ext: this.file.name.split('.').pop()
      })
      if (uploaded) {
        // 秒传
        return this.$message.success('秒传成功！')
      }
      // 切片上传
      this.chunks = chunks.map((chunk, index) => {
        // 切片的名称 hash + index
        const name = hash + '-' + index
        return {
          hash,
          name,
          index,
          chunk: chunk.file,
          // 已经上传的设置为100
          progress: uploadedList.includes(name) ? 100 : 0
        }
      })

      await this.uploadChunks(uploadedList)
      // const form = new FormData()
      // form.append('name', 'file')
      // form.append('file', this.file)
      // const ret = await this.$http.post('/uploadfile', form, {
      //   onUploadProgress: (progress) => {
      //     this.uploadProgress = Number(((progress.loaded / progress.total) * 100).toFixed(2))
      //   }
      // })
      // if (ret.code === 0) {
      //   this.$message({
      //     message: '文件上传成功',
      //     type: 'success'
      //   })
      // }
    },
    async uploadChunks (uploadedList) {
      // 过滤上传切片
      const requests = this.chunks
        .filter(chunk => !uploadedList.includes(chunk.name))
        .map((chunk, index) => {
          // 转成promise
          const form = new FormData()
          form.append('chunk', chunk.chunk)
          form.append('hash', chunk.hash)
          form.append('name', chunk.name)
          return { form, index: chunk.index, error: 0 }
        })
        // 发送所有请求
        // .map(({ form, index }) => this.$http.post('/uploadfile', form, {
        //   onUploadProgress: (progress) => {
        //     // 不是整体的进度条，而是每个区块的自己的进度条，整体的的进度条需要计算
        //     this.chunks[index].progress = Number(((progress.loaded / progress.total) * 100).toFixed(2))
        //   }
        // }))
      // @todo 并发量的控制
      // 尝试申请tcp链接过多，或造成卡顿
      // 异步的并发控制
      // await Promise.all(requests)
      await this.sendReuest(requests)
      await this.mergeRequest()
    },
    // tcp慢启动，先上传一个初始区块，比如10kb，根据上传成功的时间，决定一个区块是20k还是50k，还是5k；
    // 在下一个一样的逻辑，可能变成100k 200k或者5k；
    // 以上达到和网速的匹配
    // 上传可能报错
    // a:报错之后，进度条变红，重试
    // b:一个切边重试失败三次，整体全部终止
    sendReuest (chunks, limit = 3) {
      // limit 并发数组
      // 一个数组，长度limit
      // [task2,task3,task4]
      return new Promise((resolve, reject) => {
        const len = chunks.length
        let count = 0
        let isStop = false
        const start = async () => {
          if (isStop) { // 终止所有任务
            return
          }
          const task = chunks.shift()
          if (task) {
            const { form, index } = task
            try {
              await this.$http.post('/uploadfile', form, {
                onUploadProgress: (progress) => {
                  // 不是整体的进度条，而是每个区块的自己的进度条，整体的的进度条需要计算
                  this.chunks[index].progress = Number(((progress.loaded / progress.total) * 100).toFixed(2))
                }
              })
              if (count === len - 1) {
                // 最后一个任务
                resolve()
              } else {
                count++
                // 启动下一个
                start()
              }
            } catch (error) {
              // 切边出错重试-插入头部立即启动
              this.chunks[index].progress = -1
              if (task.error < 3) {
                task.error++
                chunks.unshift(task)
                start()
              } else { // 终止全部任务
                isStop = true
                reject(new Error('上传失败'))
              }
            }
          }
        }
        while (limit > 0) {
          // // 模拟一下延时
          // setTimeout(() => {
          //   start()
          // }, Math.random() * 2000)
          // 启动limit任务
          start()
          limit -= 1
        }
      })
    },
    mergeRequest () {
      this.$http.post('mergefile', {
        ext: this.file.name.split('.').pop(),
        size: CHUNK_SIZE,
        totalSize: this.file.size,
        hash: this.hash
      })
    }
  }
}
</script>

<style lang="stylus">
#drag
  height 100px
  line-height 100px
  border 2px dashed #eee
  text-align center
  vertical-align middle
.cube-container
  .cube
    width 14px
    height 14px
    line-height 12px
    border 1px black solid
    background #eee
    float left
    .success
      background green
    .uploading
      background blue
    .error
      background red
</style>
